深入理解 Java 线程池
(点击上方公众号,可快速关注)
来源:虾扯人生
今天写代码的时候,用到了线程池,但是由于资源有限,有可能有的任务可能会被丢弃,由于是回调第三方接口,所以我想把丢弃掉的任务信息记录到日志里,方便后续问题定位。这就需要自定义任务拒绝策略。回想到面试遇到的一些线程池问题,决定整理一下相关信息,所以这篇文章就诞生了。
一、如何构建线程池?
我相信多数用过线程池的Java程序员都用过Executors来创建线程池,该类提供了几个静态方法,可以快速创建线程池。
如上图所示,可以创建四种类型的线程池
固定线程数量的线程池。
根据需要创建线程的线程池。
执行定时任务的线程池。
单个线程的线程池。
多数情况下,这几种类型的线程池就能满足我们的需要。但是实际上还有一个创建线程池的方法那就是手动构造线程池(ThreadPoolExecutor)
二、ThreadPoolExecutor
如果你去看一下前面提到的Executors的几个静态方法的实现,你会发现他们其实就用到了ThreadPoolExecutor,只是根据不同的场景传入了不同的参数。完整的ThreadPoolExecutor总共有七个参数 如图
corePoolSize。核心线程数
maximumPoolSize。最大线程数
keepAliveTime。线程存活时间
unit。 存活时间的单位
workQueue。 工作队列
threadFactory。构造线程池中线程的工厂
handler。任务不能被处理时的拒绝策略
三、工作原理(流程)
上面列出的 几个参数,我虽然都给了中文解释,但是如果不结合原理来描述一下他们的具体作用,有些参数我感觉还是不好理解。所以这里就把线程池工作原理和参数一起讲。
提交任务的时候,判断当前线程池中的存活线程数量是否小于corePoolSize
如果小于corePoolSize,则不管是否有线程处于空闲状态,都会新建一个线程。
如果线程数量已经达到corePoolSize,则将任务扔进队列workQueue
随着任务越来越多,队列可能已经满了,则需要看当前线程是否已经达到了maximumPoolSize,如果没有达到,则创建新的线程,并用它执行该任务。
最坏情况,任务实在太多了,队列已经满了,而线程数量已经达到maximumPoolSize,还有新的任务来,说白了,就是已经满负荷了,任然还有任务需要执行,这个时候就会handler来处理该任务了。
整个工作原理就这样了,标红部分尤其重要,面试稍微深入一点,肯定就会问到这个,一定记住,是先将任务扔进队列,队列满了之后才会继续考虑创建线程。至于为什么要这样设计,可以想一想,想不通可以给我发消息。整个原理说下来,还有3个参数没有提到,这里再说明一下他们的作用
keepAliveTime。如我们所知,使用线程池的目的就是为了减少线程的创建,因为创建线程本身是比较耗资源的。由于线程本身需要占用资源,有一种情况就是,某个时候线程数量比较多,但是任务没有多少,就会出现有的线程没有活干,所以我们就可以考虑释放掉其资源,但是呢,我们又无法预知未来的任务量,所以我们就准许其空闲一段时间,如果过了这段时间都还是空闲的,那么就会释放掉其资源(就像在公司上班,可能有段时间没活干,老板可能并不会让你走,要是长时间没活干,老板可能就为了节约成本,要裁员了),这个参数就是用指定这段空闲时间的。默认情况下是有超过corePoolSize个线程时,就会用到该值, 但是也可以指定corePoolSize数量之内的线程空闲时是否释放资源(allowCoreThreadTimeout)。(就类似默认情况下,公司肯定只会裁掉非核心员工,但是实在混不走的时候,核心员工可能也会被干掉)
unit 这个参数很好理解,就是单位,就是前面keepAliveTime这个我们准许空闲的时间的单位
factory .其类型为ThreadFactory,顾名思义,就是一个创建Thread的Factory. 该接口只有一个方法,产生一个Thread。通常情况下,我们都只需要使用默认的factory就可以了,但是为了定位问题方便,我们可以为线程池创建的线程设置一个名字,这样看日志的时候就比较方便了。
四、RejectExecutionHandler
这个拒绝策略有必要拿出来单独说一下,我今天就是实现了该接口,从而满足了业务需要。
为什么我需要自己实现该接口,而采用Executors静态方法时,并没有让传入该参数呢?实际上Jdk本身提供了四种策略,分别是
AbortPolicy。会抛出异常
CallerRunnerPolicy。在调用execute的方法中执行被拒绝的任务
DiscardOldestPolicy。丢掉队列中最老的任务,然后重试
DiscardPolicy。直接丢掉该任务
这四种策略是ThredPoolExecutor的内部类,实现都比较简单,有兴趣的可以看一下。我今天的实现方式也很简单,实际上就是在discardPolicy的基础上增加日志记录。
五、其他
前面说了其工作原理,但是看了一下源代码,其实和描述的原理并不完全一致。主要在处理队列大小的时候,主要是对正在运行的线程数量还个判断,不能超过指定的值,当然这个值比较大,我们一般不会达到这个值,至于具体原因我也么去继续深入研究。
其次就是参数设置,可能需要具体业务场景,任务数量,任务执行速度来调整,并没有一个固定的值。只是记得一定要设置队列大小,不然就使用了一个无界队列,可能就是会内存爆掉。
实际使用场景下,还有一些可以优化的地方,比如对不同类型的任务创建不同的线程池, 比如有的线程比较耗时,有的很快,如果放在同一个线程池里面执行,可能导致队列很快就满了,本来该很快执行完的任务却一直得不到执行。
另外还有ThreadPoolExecutor还提供了一些hook方法,如有需要可以使用
beforeExecute() 任务执行之前调用
afterExecute() 任务执行之后调用
虽然有点标题党,说是深入理解,其实也并不是特别深入,但是基本上这篇文章的内容掌握过后,个人觉得起码90%以上的问题都能对答如流了。还有10%在哪里? 可以去看一下newCachedThreadPool的实现,他使用的队列不一样。还有就是想一想,如果实现线程存活的功能,让自己实现,怎么来做。另外可能就是真的需要去看一下源码,把具体的worker创建运行过程都搞透彻了。
【关于投稿】
如果大家有原创好文投稿,请直接给公号发送留言。
① 留言格式:
【投稿】+《 文章标题》+ 文章链接
② 示例:
【投稿】《不要自称是程序员,我十多年的 IT 职场总结》:http://blog.jobbole.com/94148/
③ 最后请附上您的个人简介哈~
看完本文有收获?请转发分享给更多人
关注「ImportNew」,提升Java技能